#include "git-repo.h"

#include <cassert>
#include <stdexcept>

#include <git2.h>

#include "util/logger/logger.h"

#include "git-cert-request.h"
#include "git-ptr-holders.h"
#include "git-fetch-context.h"

namespace
{

class LibGit2Initializer
{
public:
    LibGit2Initializer() {
        const int n = git_libgit2_init();
        if (n < 0) {
            LOGE << "git_libgit2_init() failed with error: " << n;
        } else {
            LOGD << "git_libgit2_init() ok";
        }
    }

    ~LibGit2Initializer() {
        const int n = git_libgit2_shutdown();
        if (n < 0) {
            LOGE << "git_libgit2_shutdown() failed with error: " << n;
        } else {
            LOGD << "git_libgit2_shutdown() ok";
        }
    }
};

static LibGit2Initializer g_libGit2Initializer;

} // namespace anonymous

namespace git
{

static std::string makeTitle(const fs::path &path) {
    std::ostringstream ss;
    ss << "Repo(" << path.generic_string() << ") ";
    return ss.str();
}

static std::string gitLastError() {
    const git_error *error = git_error_last();
    if (error) {
        std::ostringstream ss;
        ss << '[';
        ss << error->klass;
        ss << ',';
        ss << (error->message ? error->message : "unknown");
        ss << ']';
        return error->message;
    }
    return "[unknown]";
}

Repo::Repo(const std::string_view &localPath)
    : abspath_(fs::absolute(std::string(localPath)))
    , title_(makeTitle(abspath_)) {

    LOGD << title() << "constructed, local path: [" << localPath << "]";
}

Repo::Repo(const std::string_view &localPath, const std::string_view &uri, const Cert &cert)
    : Repo(localPath) {

    open(uri, cert);
}

Repo::~Repo() {
    LOGD << title() << "destructed";
    close("destruct");
}

void Repo::open(const std::string_view &uri, const Cert &cert) {
    if (repo_) {
        throw std::logic_error(title() + "already opened");
    }

    LOGD << title() << "open with uri [" << uri << "]";

    repo_ = doOpen(uri, cert);
    if (repo_) {
        const Head h = head();
        if (const LocalBranch *b = h.branch()) {
            LOGD << title() << "opened, HEAD attached at [" << b->fullname() << "]";
        } else if (const Commit *c = h.commit()) {
            LOGD << title() << "opened, HEAD detached at [" << c->oidHex() << "]";
        } else {
            LOGE << title() << "HEAD state error";
        }
    } else {
        LOGE << title() << "open [" << uri << "] failed";
    }
}

void Repo::close(const char *reason) {
    if (!repo_) {
        return;
    }
    
    if (reason) {
        LOGD << title() << "close by reason: [" << reason << "]";
    } else {
        LOGD << title() << "close";
    }

    git_repository_free(repo_);
    repo_ = nullptr;
}

bool Repo::isHeadDetached() const {
    const int n = git_repository_head_detached(repo_);
    if (n == 1) {
        return true;
    }
    if (n == 0) {
        return false;
    }
    LOGE << title() << "git_repository_head_detached error: " << gitLastError();
    return true;
}

Head Repo::head() const {
    assert(repo_);

    RefHolder headref;
    git_repository_head(headref.visit(), repo_);
    if (!headref) {
        LOGE << title() << "git_repository_head error: " << gitLastError();
        return Head();
    }

    if (!isHeadDetached()) {
        return LocalBranch(headref.take());
    }

    const git_oid *commitId = git_reference_target(headref.look());
    if (!commitId) {
        LOGD << title() << "git_reference_target cannot get target";
        return Head();
    }

    git_commit *commit = nullptr;
    git_commit_lookup(&commit, repo_, commitId);
    if (!commit) {
        LOGD << title() << "git_commit_lookup cannot find commit";
        return Head();
    }

    return Head(Commit(commit));
}

void Repo::logDump() const {
    assert(repo_);

    git::Head h = head();
    
    if (const git::LocalBranch *b = h.branch()) {
        LOGI << title() << "HEAD is attached at branch: [" << b->fullname() << "]";
        git::Upstream up = b->upstream();
        if (git::RemoteTrackBranch *remoteTrack = up.asRemoteTrackBranch()) {
            LOGI << title() << "HEAD's upstream is remote track branch: [" << remoteTrack->fullname() << "]";
            if (git::Remote remote = remoteTrack->remote()) {
                LOGI << title() << "HEAD's associated remote name is [" << remote.name() << "], url is [" << remote.uri() << "]";
            } else {
                LOGI << title() << "HEAD's associated remote is error";
            }
        } else if (git::LocalBranch *local = up.asLocalBranch()) {
            LOGI << title() << "HEAD's upstream is local branch: [" << local->fullname() << "]";
        } else {
            LOGI << title() << "HEAD do not have upstream";
        }
    } else if (const git::Commit *c = h.commit()) {
        LOGI << title() << "HEAD is detached at commit: [" << c->oidHex() << "]";
    } else {
        LOGI << title() << "HEAD is error";
    }
}

void Repo::pull(const Cert &cert) {
    LOGI << title() << "pull start";

    const Head h = head();

    const LocalBranch *localBranch = h.branch();
    if (!localBranch) {
        if (h) {
            LOGI << title() << "pull stop because HEAD is detached";
        } else {
            LOGE << title() << "pull stop because HEAD is error";
        }
        return;
    }

    Upstream upstream = localBranch->upstream();
    RemoteTrackBranch *remoteTrackBranch = upstream.asRemoteTrackBranch();
    if (!remoteTrackBranch) {
        if (upstream) {
            LOGI << title() << "pull stop because HEAD's upstream is not remote track branch";
        } else {
            LOGE << title() << "pull stop because HEAD's upstream is error";
        }
        return;
    }

    Remote remote = remoteTrackBranch->remote();
    if (!remote) {
        LOGE << title() << "pull stop because HEAD's upstream's remote is error";
        return;
    }

    LOGI << title() << "pull fetch [" << remote.name() << "] start";
    remote.fetch(cert);
    LOGI << title() << "pull fetch [" << remote.name() << "] stop";

    Upstream newUpstream = localBranch->upstream();
    RemoteTrackBranch *newRemoteTrackBranch = newUpstream.asRemoteTrackBranch();
    if (!newRemoteTrackBranch) {
        LOGE << "pull stop because remote track branch error after fetch";
        return;
    }

    LOGI << title() << "pull merge start";
    merge(*newRemoteTrackBranch);
    LOGI << title() << "pull merge stop";

    LOGI << title() << "pull stop";
}

void Repo::merge(const Branch &otherBranch) {
    assert(repo_);

    LOGI << title() << "merge branch [" << otherBranch.fullname() << "] into HEAD start";

    const Head h = head();
    const LocalBranch *headBranch = h.branch();
    if (!headBranch) {
        LOGE << title() << "merge stop because HEAD is detached";
        return;
    }

    git_merge_options opt;
    git_merge_options_init(&opt, GIT_MERGE_OPTIONS_VERSION);

    const git_repository_state_t repoState = static_cast<git_repository_state_t>(git_repository_state(repo_));
    if (repoState != GIT_REPOSITORY_STATE_NONE) {
        LOGE << title() << "merge stop because the repo is not in normal state";
        return;
    }

    LOGD << title() << "merge step 'git_repository_state' done, repo is in normal state";

    AnnotatedCommitHolder annotatedCommitHolder;
    git_annotated_commit_from_ref(annotatedCommitHolder.visit(), repo_, otherBranch.ref_);
    if (!annotatedCommitHolder) {
        LOGE << title() << "merge stop after 'git_annotated_commit_from_ref' with error: " << gitLastError();
        return;
    }

    LOGD << title() << "merge step 'git_annotated_commit_from_ref' done";

    git_merge_analysis_t analysisResult = GIT_MERGE_ANALYSIS_NONE;
    git_merge_preference_t mergePref = GIT_MERGE_PREFERENCE_NONE;
    const git_annotated_commit *annotedCommitPtr = annotatedCommitHolder.look();
    const int analysisErr = git_merge_analysis(&analysisResult, &mergePref, repo_, &annotedCommitPtr, 1);
    if (analysisErr != 0) {
        LOGE << title() << "merge stop after 'git_merge_analysis' with error: " << gitLastError();
        return;
    }

    LOGD << title() << "merge step 'git_merge_analysis' done";

    if (analysisResult & GIT_MERGE_ANALYSIS_UP_TO_DATE) {
        LOGD << title() << "merge done because already up to date";
        return;
    }

    LOGD << title() << "merge should be executed because not up to date";
    
    if ((analysisResult & GIT_MERGE_ANALYSIS_FASTFORWARD) && !(mergePref & GIT_MERGE_PREFERENCE_NO_FASTFORWARD)) {
        const git_oid *otherBranchOid = git_annotated_commit_id(annotatedCommitHolder.look());

        git_object *otherObject = nullptr;
        git_object_lookup(&otherObject, repo_, otherBranchOid, GIT_OBJECT_COMMIT);
        if (!otherObject) {
            LOGE << title() << "git_object_lookup error: " << gitLastError();
            return;
        }

        char buffer[1024] = {};

        LOGD << "otherBranchOid: " << git_oid_tostr(buffer, 1023, otherBranchOid);

        git_checkout_options options;
        git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION);
        options.checkout_strategy = GIT_CHECKOUT_SAFE;
        const int checkoutResult = git_checkout_tree(repo_, otherObject, &options);
        if (checkoutResult < 0) {
            LOGE << title() << "git_checkout_tree error: " << gitLastError();
            return;
        }

        LOGD << title() << "git_checkout_tree done";

        RefHolder newRef;

        const int setRefTargetErr = git_reference_set_target(newRef.visit(), headBranch->ref_, otherBranchOid, "fast forward");
        if (setRefTargetErr != 0) {
            LOGE << title() << "merge by fast-forward with error: " << gitLastError();
            return;
        }

        LOGD << title() << "merge by fast-forward done";

        return;
    }

    LOGE << "unsupported merge type";
}

git_repository *Repo::openExistedLocalRepo() const {
    git_repository *repo = nullptr;
    git_repository_open(&repo, abspath_.generic_string().c_str());
    if (!repo) {
        LOGE << title() << "git_repository_open error: " << gitLastError();
    }
    return repo;
}

git_repository *Repo::cloneRemoteRepo(const std::string_view &uri, const Cert &cert) const {
    git_clone_options opt;
    fillGitCloneOption(opt);

    const FetchContext context(cert, opt.fetch_opts);

    LOGI << title() << "git_clone start";

    git_repository *repo = nullptr;
    git_clone(&repo, std::string(uri).c_str(), abspath_.generic_string().c_str(), &opt);

    LOGI << title() << "git_clone stopped";

    if (!repo) {
        LOGE << "git_clone error: " << gitLastError();
    }

    return repo;
}

git_repository *Repo::doOpen(const std::string_view &uri, const Cert &cert) const {
    if (fs::exists(abspath_)) {
        if (fs::is_directory(abspath_)) {
            return openExistedLocalRepo();
        } else {
            LOGE << title() << "local path [" << abspath_.generic_string() << "] exists but is not dir";
            return nullptr;
        }
    } else {
        return cloneRemoteRepo(uri, cert);
    }
}

void Repo::fillGitCloneOption(git_clone_options &opt) const {
    git_clone_options_init(&opt, GIT_CLONE_OPTIONS_VERSION);
    opt.checkout_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
    fillGitFetchOption(opt.fetch_opts);
}

void Repo::fillGitFetchOption(git_fetch_options &opt) const {
    opt = GIT_FETCH_OPTIONS_INIT;
}


} // namespace git
